﻿-- Spring Max
-- this is a tool for create bone chain follow throught animation
-- features:
-- wave, twist animations
-- strach and squash effect
-- collision with capsule body and plane
-- can apply wind or explosion force

-- Yanbin Bai
-- 2018.08.18
-- animbai.com

-- if you can see these word, means you already decrypted script, and you have script code skill
-- so if you want to improve this tool, or to know how can this tool work, plz send me mail at
-- redtank@outlook.com
-- i'll be glade to answer your questions :)


-- Version History:

-- 1.0
-- First public build

sm_ver = 1.0

aimRefLoc_subFix = "_SpringMaxRefLoc"
aimChildRefLoc_subFix = "_SpringMaxChildRefLoc"
aimCurLocBase_subFix = "_SpringMaxCurLocBase"
aimCurLoc_subFix = "_SpringMaxCurLoc"
aimPreLoc_subFix = "_SpringMaxPreLoc"
aimUpLoc_subFix = "_SpringMaxUpLoc"
aimRefUpLoc_subFix = "_SpringMaxRefUpLoc"
aimPreUpLoc_subFix = "_SpringMaxPreUpLoc"
colBody_subFix = "_SpringMaxColBody"
colPlane_subFix = "_SpringMaxColPlan"
windCone_subFix = "_SpringMaxWind"
expCtrl_subFix = "_SpringMaxExplosion"
connectSpline_subFix = "SpringMaxSpline"

fn getObjWithName objStr exact:False =(
    objLst = #()
    if exact then(
        obj = execute ("$'"+objStr+"'")
        append objLst obj
    )
    else objLst = execute ("$*'"+objStr+"*'")
    if objLst.count>0 then(
        objLst
    )
    else undefined
)

fn sm_getLoc obj locatorSubFix =(
    (getnodebyname (obj.name+locatorSubFix) exact:true)
)

fn sm_addPosCons boneObj fNeibour bNeibour =(
    smBonePreUpLoc = sm_getLoc boneObj aimRefUpLoc_subFix
    fRefLoc = undefined
    if fNeibour != undefined then(
        fRefLoc = sm_getLoc fNeibour aimRefLoc_subFix
    )
    bRefLoc = undefined
    if bNeibour != undefined then(
        bRefLoc = sm_getLoc bNeibour aimRefLoc_subFix
    )

    posCtrl = Position_Constraint()
    nloc = undefined
    if fRefLoc != undefined then(
        posCtrl.appendTarget fRefLoc 0.5
    )
    if bRefLoc != undefined then(
        posCtrl.appendTarget bRefLoc 0.5
    )
    posCtrl.RELATIVE = on

    smBonePreUpLoc.pos.controller = posCtrl
)

fn sm_addAimCons aimbone refLoc preLoc upLoc weight tension isTension =( -- fNeibour bNeibour 

    aimCtrl = LookAt_Constraint()
    aimCtrl.appendTarget refLoc weight
    aimCtrl.appendTarget preLoc (1-weight)
--     nloc = undefined
--     if fNeibour != undefined then(
--         nloc = sm_getLoc fNeibour aimRefLoc_subFix
--         if nloc != undefined then(
--             aimCtrl.appendTarget nloc (tension*0.5)
--         )
--     )
--     if bNeibour != undefined then(
--         nloc = sm_getLoc bNeibour aimRefLoc_subFix
--         if nloc != undefined then(
--             aimCtrl.appendTarget nloc (tension*0.5)
--         )
--     )
--     smBonePreUpLoc = sm_getLoc aimbone aimRefUpLoc_subFix
--     aimCtrl.appendTarget smBonePreUpLoc (tension*0.5)

    -- add tension aim
    tensionLoc = sm_getLoc aimbone aimChildRefLoc_subFix
    tensionWeight = 0.0
    if isTension and (tensionLoc!=undefined) then(
--         tensionWeight = (1-weight)*tension
        aimCtrl.appendTarget tensionLoc tensionWeight
    )

    aimCtrl.viewline_length_abs = False
    aimCtrl.lookat_vector_length = 0

    aimCtrl.upnode_world = False
    aimCtrl.pickUpNode = upLoc
--     aimbone.rotation.controller = aimCtrl
    -- add aim constraint into rotation list
    aimbone.rotation.controller = Rotation_list()
    aimbone.rotation.controller.available.controller = aimCtrl
    aimbone.rotation.controller.setactive 2 --otherwise, EulerXYZ was active
--     aimCtrl.RELATIVE = on
)

fn sm_addOriCons curUpLoc preUpLoc refUpLoc weight=(
    oriCtrl = Orientation_Constraint()
    oriCtrl.appendTarget refUpLoc weight
    oriCtrl.appendTarget preUpLoc (1-weight)

    curUpLoc.rotation.controller = oriCtrl
)

fn collapseTransform obj startFrame endFrame subDiv isTCB= (
    -- same function as collapse transform in Trajections panel
    local TCtrller = Position_XYZ()
    if not isTCB then(
        local RCtrller = Euler_XYZ()
    )
    else(
        local RCtrller = TCB_Rotation() -- TCB can get smoother result for long chain
    )
    local pObj = obj.parent

    startFrame = animationRange.start
    endFrame = animationRange.end

    with animate on (
        for i in startFrame to endFrame by 1/subDiv do (
            at time i(
                local m = (if (pObj!=undefined) then pObj.transform else (matrix3 1))
                local currm = obj.transform
                local newm = currm * (Inverse m)
                TCtrller.value = newm.translationpart
                RCtrller.value = newm.rotationpart
            )
        )
    )
    obj.pos.controller = TCtrller

    obj.rotation.controller = RCtrller

)

fn sm_createLocator obj subfix boneLocLst isChildTransform isParent isDraw =(
    if obj.children.count>0 then(
        local objChild = obj.children[1]
        local locator = point name:(obj.name+subfix) cross:isDraw
        if isChildTransform then(
            locator.transform = objChild.transform
        )
        else(
            locator.transform = obj.transform
        )
        if isParent then(
            locator.parent = obj.parent
        )
        append boneLocLst locator
        locator
    )
    else(
        undefined
    )
)

fn sm_createWindCone =(

    -- remove exist wind cone
    if (getObjWithName windCone_subFix) != undefined then(
        delete (getObjWithName windCone_subFix)[1]
    )

    windCone = cone name:("the"+windCone_subFix)
    windCone.xray = on
    windCone.renderable = off
    -- add new modifier
    m = EmptyModifier name:"Spring Max Force"
    ca = attributes forceAttr
    (
        parameters params rollout:params
        (
            maxForce type:#float ui:windMaxForce
            minForce type:#float ui:windMinForce
            frequency type:#float ui:windFrequency
        )
        rollout params "Wind"
        (
            spinner windMaxForce "Max Force:" type:#float
            spinner windMinForce "Min Force:" type:#float
            spinner windFrequency "Frequency:" type:#float
        )
    )
    custattributes.add m ca
    addmodifier windCone m

    windCone.modifiers[#Spring_Max_Force].forceAttr.maxForce = 10.0
    windCone.modifiers[#Spring_Max_Force].forceAttr.minForce = 5.0
    windCone.modifiers[#Spring_Max_Force].forceAttr.frequency = 0.15
)

fn sm_createExpCtrl =(

    -- remove exist wind cone
    if (getObjWithName expCtrl_subFix) != undefined then(
        delete (getObjWithName expCtrl_subFix)[1]
    )

    ExpCtrl = Hedra name:("the"+expCtrl_subFix)
    ExpCtrl.xray = on
    ExpCtrl.renderable = off
    -- add new modifier
    m = EmptyModifier name:"Spring Max Force"
    ca = attributes forceAttr
    (
        parameters params rollout:params
        (
            maxForce type:#float ui:windMaxForce
            minForce type:#float ui:windMinForce
            frequency type:#float ui:windFrequency
            isReverse type:#boolean ui:windIsReverse
        )
        rollout params "Explosion"
        (
            spinner windMaxForce "Max Force:" type:#float
            spinner windMinForce "Min Force:" type:#float
            spinner windFrequency "Frequency:" type:#float
            checkbox windIsReverse "Reverse" type:#boolean
        )
    )
    custattributes.add m ca
    addmodifier ExpCtrl m

    ExpCtrl.modifiers[#Spring_Max_Force].forceAttr.maxForce = 50.0
    ExpCtrl.modifiers[#Spring_Max_Force].forceAttr.minForce = 30.0
    ExpCtrl.modifiers[#Spring_Max_Force].forceAttr.frequency = 0.5
)

fn offsetPosByDirection sDirection tPos moveDistance =(
    -- get new target pos
    tPos += (sDirection*moveDistance)

    tPos
)

fn sigmoid x =(
  (1 / (1 + exp(-x)))
)

fn sm_distance a b =(
    length(b-a)
)

fn lerp_vec a b t =(
    (a*(1-t) + b*t)
)

fn dist_to_plane pt n d =(
    -- pt: point pos
    -- n: plane direction
    -- q: a point pos on plane
    -- d = dot n q
    -- return: distance to plane, - means on back side
    -- this function calculate plane as a infinity size plane
    ((dot n pt) - d/(dot n n))
)

fn dist_to_line a b p =(
    ap = p-a
    ab = b-a
    dap = dot ap ab
    dab = dot ab ab
    result = a + dap/dab * ab
    (sm_distance result p)
)

fn is_same_side_of_plane pt test_pt n d =(
    d1 = 1.0
    if (dist_to_plane pt n d) < 0 then(
        d1 = -1.0
    )
    d2 = 1.0
    if (dist_to_plane test_pt n d) < 0 then(
        d2 = -1.0
    )
    ((d1*d2) == 1.0)
)

fn proj_pt_to_plane pt  n  d=(
    t = (dot n pt) - d
    (pt - n*t)
)

fn pt_in_sphere pt c r =(
    (sm_distance c pt <= r)
)

fn pt_in_cylinder pt p q r=(
    n = normalize(q-p)
    d = dot n p
    if not (is_same_side_of_plane pt ((p+q)/2.0) n d) then False
    n = normalize(q-p)
    d = dot n q
    if not (is_same_side_of_plane pt ((p+q)/2.0) n d) then False
    proj_pt = proj_pt_to_plane pt n d
    ((sm_distance proj_pt q) <= r)
)

fn segment_sphere_isect sa sb c r =(
    NotFound = #(False, None)
    
    p = sa
    d = normalize(sb-sa)

    m = p - c
    b = dot m d
    c = (dot m m) - r * r

    if (c > 0.0) and (b > 0.0) then NotFound

    discr = b*b - c
    if discr < 0.0 then NotFound

    t = -b - sqrt(discr)
    if t < 0.0 then NotFound

    dist = sm_distance sa sb
    q = p + d * t
    #((t>=0 and t<=dist), q)
)

fn segment_cylinder_isect sa sb p q r=(
    SM_EPSILON = 1e-6
    d = q-p
    m = sa-p
    n = sb-sa
    md = dot m d
    nd = dot n d
    dd = dot d d

    NotFound = #(False, None)
    if (md < 0) and ((md + nd) < 0) then NotFound
    if (md > dd) and ((md + nd) > dd) then NotFound

    nn = dot n n
    mn = dot m n

    a = dd*nn - nd*nd
    k = (dot m m) - r*r
    c = dd*k - md*md

    if abs(a) < SM_EPSILON then(
        if c > 0 then NotFound
        if (md < 0)or(md > dd) then(
            if md < 0 then(
                t = -mn / nn
            )
            if md > dd then(
                t = (nd - mn) / nn
            )
        )
        else(
            t = 0
        )
        #(True, (lerp_vec sa sb t))
    )

    b = dd*mn - nd*md
    discr = b*b - a*c
    if discr < 0 then NotFound
    t = (-b - sqrt(discr))/a
    if (t < 0.0) or (t > 1.0) then NotFound
    if ((md + t * nd) < 0.0) then(
        if nd <= 0.0 then NotFound
        t = (-1*md)/nd
        #((k + 2*t*(mn + t*nn) <= 0.0), (lerp_vec sa sb t))
    )
    if (md + t*nd) > dd then(
        if nd >= 0.0 then NotFound
        t = (dd - md)/nd
        #(((k + dd - 2*md + t*(2*(mn - nd) + t*nn)) <= 0.0), (lerp_vec sa sb t))
    )
    #(True, (lerp_vec sa sb t))
)

fn pt_in_capsule pt p q r =(
    ((pt_in_cylinder pt p q r) or (pt_in_sphere pt p r) or (pt_in_sphere pt q r))
)

fn segment_capsule_isect sa sb p q r =(

    if (pt_in_capsule sa p q r) and (pt_in_capsule sb p q r) then(
        -- both inside.  extend sb to get intersection
        newb = sa + normalize(sb-sa) * 200.0
        sa = newb
        sb = sa
    )
    if (pt_in_capsule sa p q r) then(
        sb = sa
        sa = sb
    )
    d = normalize(sb-sa)

    i1 = segment_sphere_isect sa sb p r
    i2 = segment_sphere_isect sa sb q r
    i3 = segment_cylinder_isect sa sb p q r

    -- make a big number
    dist = 3.40E38

    closest_pt = None
    hit = False
    hitCylinder = False
    if i1[1] then(
        hit = True
        pt = i1[2]
        if (sm_distance sa pt) < dist then(
            closest_pt = pt
        )
        dist = amin #(dist, (sm_distance sa pt))
        -- draw_locator(i1[2], 'i1')
    )
    if i2[1] then(
        hit = True
        pt = i2[2]
        if (sm_distance sa pt) < dist then(
            closest_pt = pt
        )
        dist = amin #(dist, (sm_distance sa pt))
    )
    if i3[1] then(
        hit = True
        hitCylinder = True
        pt = i3[2]
        if (sm_distance sa pt) < dist then(
            closest_pt = pt
        )
        dist = amin #(dist, (sm_distance sa pt))
    )
    #(hit, closest_pt, hitCylinder)
)


fn getColCapsulePQ colBody =(

    r = colBody.radius
    h = colBody.height
    m = matrix3 1
    m.pos = [-r,0,0]
    p = (inverse m*colBody.transform).pos -- sphereA center pos of capsule
    m.pos = [-(h-r),0,0]
    q = (inverse m*colBody.transform).pos -- sphereB center pos of capsule
    #(p,q,r,h)
)

fn sm_flipObj obj =(
    t = obj.transform
    obj.transform *= inverse obj.transform
    obj.rotation *= quat 1 1 0 0
    obj.transform *= t
)

fn preCheckCollision obj colBodyLst =(
    -- pre check bone length compare with collision body radius
    -- will improve performance if bone is far from capsule
    objLength = obj.boneLength
    for colBody in colBodyLst do(
        pq = getColCapsulePQ colBody
        p = pq[1]
        q = pq[2]
        boneToCapsuleDistance = dist_to_line p q obj.pos
        r = pq[3]

        if boneToCapsuleDistance < (objLength+r) then return #(p,q)  -- means close enough to have a hit change
    )
    undefined
)

fn ckeckPointInTri pos pa pb pc =(
    ra = acos(dot (normalize (pa-pos)) (normalize(pb-pos)))
    rb = acos(dot (normalize (pb-pos)) (normalize(pc-pos)))
    rc = acos(dot (normalize (pc-pos)) (normalize(pa-pos)))
    (abs(ra+rb+rc)>359)
)

fn getVertexPositions obj = (
    local vPositions = #()
    local triMesh = snapshotAsMesh obj
    for v = 1 to triMesh.numVerts do (
        append vPositions (getVert triMesh v)
    )
    delete triMesh
    vPositions
)

fn checkPlaneCollision obj refLoc colPlane=(

    v_coords = getVertexPositions colPlane

    -- get obj distance to plane
    n = colPlane.dir
    q = v_coords[1]
    d = dot n q
    pt = obj.pos
    toPlaneDistance = dist_to_plane pt n d
    pt_child = refLoc.pos
    toPlaneDistance_child = dist_to_plane pt_child n d

    -- get project pos on plane
    projectPos_child = proj_pt_to_plane pt_child n d
    inPlane = False
    if (ckeckPointInTri projectPos_child v_coords[1] v_coords[2] v_coords[3]) then inPlane = True
    else(
        if (ckeckPointInTri projectPos_child v_coords[4] v_coords[2] v_coords[3]) then inPlane = True
    )

    -- bone above plane and bone child under plane and child project point on plane
    -- means has collision with plane
    if (toPlaneDistance>0) and (toPlaneDistance_child<0) and inPlane then projectPos_child
    else undefined
)

-- use ray
-- fn checkCollision cur_pos pre_pos capsuleLst isRevert =(
--     -- calculate collision with all the capsule in scene
--     if isRevert then(
--         sa = cur_pos
--         sb = pre_pos
--     )
--     else(
--         sb = cur_pos
--         sa = pre_pos
--     )

--     isHited = False
--     closest_pt_dict = #()
--     for colBody in capsuleLst do(
--         r = colBody.radius
--         h = colBody.height
--         m = matrix3 1
--         m.pos = [-r,0,0]
--         p = (inverse m*colBody.transform).pos -- sphereA center pos of capsule
--         m.pos = [-(h-r),0,0]
--         q = (inverse m*colBody.transform).pos -- sphereB center pos of capsule

--         hitResult = segment_capsule_isect sa sb p q r
--         hit = hitResult[1]
--         closest_pt = hitResult[2]
--         hitCylinder = hitResult[3]


--         if hit then(
--             isHited = True
--             append closest_pt_dict #(colBody,closest_pt)
--             -- drawDebug_box(closest_pt)
--         )
--     )
--     if isHited then(
--         pt_length = 3.40E38
--         closest_pt = undefined
--         col_obj = undefined
--         for pt in closest_pt_dict do(
--             lLength = sm_distance pre_pos pt[2]
--             if lLength < pt_length then(
--                 pt_length = lLength
--                 closest_pt = pt[2]
--                 col_obj = pt[1]
--             )
--         )
--         -- return col pt and col_body speed
--         return #(closest_pt, col_obj, hitCylinder)
--     )
--     else(
--         return #(undefined, undefined, undefined)
--     )
-- )

fn sm_applyForce targetObj forceObj subDiv offset isExp =(
    offset = offset as float
    if offset>0 then(    -- skip on first frame
        maxForce = forceObj.modifiers[#Spring_Max_Force].forceAttr.maxForce
        minForce = forceObj.modifiers[#Spring_Max_Force].forceAttr.minForce
        freq = forceObj.modifiers[#Spring_Max_Force].forceAttr.frequency

        midForce = (maxForce+minForce)/2

        -- get source x-axis direction in world space
        forceDir = undefined
        disOffset = 0.0
        if isExp then(
            isReverse = forceObj.modifiers[#Spring_Max_Force].forceAttr.isReverse
            if isReverse then(
                forceDir = normalize (forceObj.pos-targetObj.pos)
            )
            else(
                forceDir = normalize (targetObj.pos-forceObj.pos)
            )
            -- get obj distance to wind plane
            n = forceObj.dir
            q = forceObj.pos
            d = dot n q
            disOffset = dist_to_plane targetObj.pos n d
        )
        else(
            forceDir = forceObj.dir
            disOffset = sm_distance forceObj.pos targetObj.pos
        )
        forceDistance = sin(offset*freq+disOffset)*(maxForce-minForce)+midForce

        targetObj.pos = offsetPosByDirection forceDir targetObj.pos (forceDistance/subDiv)
    )
)


fn checkCollision cur_pos pre_pos capsuleLst isRevert fastMoveNum subDiv =(
    -- checkCollision with ray
    if not isRevert then(
        rRay = ray cur_pos (pre_pos-cur_pos)
    )
    else(
        rRay = ray pre_pos (cur_pos-pre_pos)
    )

    closest_pt_dict = #()
    for colBody in capsuleLst do(
        pqr = getColCapsulePQ colBody
        with animate off(
            colBody.curPosP = pqr[1]
            colBody.curPosQ = pqr[2]
        )
        r = pqr[3]
        h = pqr[4]
        is_cur_pos_inside = pt_in_capsule cur_pos colBody.curPosP colBody.curPosQ r
        is_pre_pos_inside = pt_in_capsule pre_pos colBody.curPosP colBody.curPosQ r
--         print is_cur_pos_inside
--         print is_pre_pos_inside

        if is_cur_pos_inside or is_pre_pos_inside then( -- skip ray check if both point out of capsult
            if is_cur_pos_inside and is_cur_pos_inside then( -- rebuild ray if both point are inside capsule
                n = amax #(r*2, h)    -- use longest side of capsule as n
                t = n/(distance cur_pos pre_pos)+1  -- get times rate between n and line length
                if not isRevert then(
                    rRay = ray ((pre_pos-cur_pos)*t+cur_pos) (cur_pos-pre_pos)
                )
                else(
                    rRay = ray ((cur_pos-pre_pos)*t+pre_pos) (pre_pos-cur_pos)
                )
            )
            result = (intersectray colBody rRay)
            -- print (result != undefined)
            if result != undefined then(
                append closest_pt_dict #(colBody,result.pos)
            )
        )
        -- if any end of Capsule moves faster than seted number, will trigger fast move
        pSpeed = (sm_distance colBody.curPosP colBody.prePosP)*subDiv
        qSpeed = (sm_distance colBody.curPosQ colBody.prePosQ)*subDiv
        with animate off(
            colBody.isFastMove = (pSpeed>fastMoveNum) or (qSpeed>fastMoveNum)
            colBody.moveSpeed = amax #(pSpeed,qSpeed)
        )
--         print #(colBody.curPosP,colBody.prePosP)
--         print pSpeed
--         print qSpeed
--         print colBody.isFastMove
    )
    if closest_pt_dict.count>0 then(
        pt_length = 3.40E38
        closest_pt = undefined
        col_obj = undefined
        for pt in closest_pt_dict do(
            lLength = distance pre_pos pt[2]
            if lLength < pt_length then(

                pt_length = lLength
                closest_pt = pt[2]
                col_obj = pt[1]

            )
        )
        -- return col pt and col_body speed
        #(closest_pt, col_obj)
    )
    else #(undefined, undefined)
)

fn sm_restoreInitPose boneLst =(
    for obj in boneLst do(
        local boneInitPos_attr = attributes boneInitPos_attr (parameters main (boneInitPos type:#point3))
        custAttributes.add obj boneInitPos_attr
        local boneInitRot_attr = attributes boneInitRot_attr (parameters main (boneInitRot type:#string))
        custAttributes.add obj boneInitRot_attr
        obj.boneInitPos = obj.pos.controller.value
        obj.boneInitRot = (obj.rotation.controller.value) as string
    )
)

fn sm_repeatMoveToPlane obj objTarget colPlane times =(
--     proxyPlane = convertToPoly(instance colPlane)
--     v_coords = for vID = 1 to proxyPlane.GetNumVertices() collect polyOp.getVert proxyPlane vID
--     delete proxyPlane

    n = colPlane.dir
    q = colPlane.pos
    d = dot n q
    for i = 1 to times do(
        pt = obj.pos
        obj.pos = proj_pt_to_plane pt n d
        if i != times then obj.pos = objTarget.pos
    )
)

fn sm_connectBone boneLst =(
    -- filter bones
    boneLst = for obj in boneLst where (classOf obj == BoneGeometry) and (obj.children != undefined) collect obj
    if boneLst.count < 2 then return False
    isClose = sm_SpringMax.springConnectClose_checkbox.state
    -- creat curve
    ss = SplineShape pos:[0,0,0]
    ss.renderable = off
    ss.name = ""
    addNewSpline ss
    local attr = attributes attr (parameters main (boneLst type:#stringTab tabSizeVariable:true))
    custAttributes.add ss attr
    local cAttr = attributes cAttr (parameters main (isClose type:#boolean))
    custAttributes.add ss cAttr
    for smBone in boneLst do(
        ss.name += smBone.name
        ss.name += "_"
        addKnot ss 1 #smooth #curve smBone.children[1].pos
        append ss.boneLst smBone.name
    )
    if isClose then(
        addKnot ss 1 #smooth #curve boneLst[1].children[1].pos
    )
    ss.name += connectSpline_subFix
    ss.isClose = isClose
    updateShape ss
)

sm_bonePosLst = #()
sm_boneRotLst = #()
fn sm_copyBoneTM boneLst=(
    sm_bonePosLst = #()
    sm_boneRotLst = #()
    for obj in boneLst do(
        append sm_bonePosLst obj.pos.controller.value
        append sm_boneRotLst obj.rotation.controller.value
    )
)
fn sm_pasteBoneTM boneLst=(
    i=1
    for obj in boneLst do(
        obj.pos.controller.value = sm_bonePosLst[i]
        obj.rotation.controller.value = sm_boneRotLst[i]
        i += 1
    )
)

fn springMax_Main boneLst spring_bend spring_twist extend tension startFrame endFrame isTargetFrame targetFrame subDiv isWipe isLoop isTcb isCollision isFastMove fastMoveNum colBodyLst colPlaneLst isWind windObj isExp expObj=(

    isFirstPass = True

    spring_bend /= subDiv
    spring_twist /= subDiv
--     tension /= subDiv

    tension /= 1/(sigmoid(1-subDiv)+0.5)

    -- create locator
    boneLocLst = #()

    -- goto target frame do to initial work
    if isTargetFrame then sliderTime = targetFrame
    else sliderTime = startFrame
    -- build locator first
    for smBone in boneLst do(
        if smBone.children.count>0 then(
            -- remove exist anim
            selectKeys smBone.pos.controller startFrame endFrame
            deleteKeys smBone.pos.controller #selection
            selectKeys smBone.rotation.controller startFrame endFrame
            deleteKeys smBone.rotation.controller #selection

            smBoneChild = smBone.children[1]
            -- ref pos
            smBoneRefLoc = sm_createLocator smBone aimRefLoc_subFix boneLocLst True True True
            -- child ref pos, only creat of whe bone has grandchild
            if smBoneChild.children.count>0 then(
                smBoneChildRefLoc = sm_createLocator smBoneChild aimChildRefLoc_subFix boneLocLst True True True
                smBoneChildRefLoc.name = smBone.name+aimChildRefLoc_subFix
                smBoneChildRefLoc.parent = smBone.parent
            )
            -- pre pos
            smBonePreLoc = sm_createLocator smBone aimPreLoc_subFix boneLocLst True False True
            -- up axis
            smBoneUpLoc = sm_createLocator smBone aimUpLoc_subFix boneLocLst False False False
            -- ref up axis
            smBoneRefUpLoc = sm_createLocator smBone aimRefUpLoc_subFix boneLocLst False True False
            -- pre up axis
            smBonePreUpLoc = sm_createLocator smBone aimPreUpLoc_subFix boneLocLst False False False

        )
    )

    -- add constraint after
    for smBone in boneLst do(
        if smBone.children.count>0 then(
--             -- use two aim that can play offset on smBoneCurLoc
--             -- make smBoneCurLocBase aim to target angle
--             sm_addAimCons smBoneCurLocBase smBoneRefLoc smBonePreLoc undefined spring_bend False False
            -- make bone aim to smBoneCurLoc
            -- find all bone loc
            smBoneRefLoc = sm_getLoc smBone aimRefLoc_subFix
            smBonePreLoc = sm_getLoc smBone aimPreLoc_subFix
            smBoneCurLocBase = sm_getLoc smBone aimCurLocBase_subFix
            smBoneCurLoc = sm_getLoc smBone aimCurLoc_subFix
            smBoneUpLoc = sm_getLoc smBone aimUpLoc_subFix
            smBonePreUpLoc = sm_getLoc smBone aimPreUpLoc_subFix
            smBoneRefUpLoc = sm_getLoc smBone aimRefUpLoc_subFix

            isTension = (tension>0.0 and isCollision)

            -- add connect bone
            cCurveLst = getObjWithName connectSpline_subFix
            fNeibourBone = undefined
            bNeibourBone = undefined
            -- get all curve
            if cCurveLst != undefined then(
                for cCurve in cCurveLst do(
                    boneIndex = 1
                    foundBone = False
                    boneLstCount = cCurve.boneLst.count
                    for boneName in cCurve.boneLst do(
                        boneObjLst = getObjWithName boneName exact:True

                        -- quit calculation if duplication bone name or cannot find bone
                        if (boneObjLst==undefined) then(
                            messageBox (boneName+uistrLst[2][5])
                            return False
--                             exit
                        )
                        if (boneObjLst.count>1) then(
                            messageBox (boneName+uistrLst[2][4])
                            return False
--                             exit
                        )
                        -- find neibour
                        if boneObjLst[1] == smBone then(
                            foundBone = True
                            -- get bone neibour if found bone in connection list
                            if boneIndex == 1 then( -- is start bone
                                if cCurve.isClose then fNeibourBone = (getObjWithName cCurve.boneLst[boneLstCount] exact:True)[1]
                                else fNeibourBone = undefined
                                bNeibourBone = (getObjWithName cCurve.boneLst[2] exact:True)[1]
                            )
                            if (boneIndex>1) and (boneIndex<boneLstCount) then( -- is middle bone
                                fNeibourBone = (getObjWithName cCurve.boneLst[boneIndex-1] exact:True)[1]
                                bNeibourBone = (getObjWithName cCurve.boneLst[boneIndex+1] exact:True)[1]
                            )
                            if boneIndex == boneLstCount then( -- is end bone
                                fNeibourBone = (getObjWithName cCurve.boneLst[boneLstCount-1] exact:True)[1]
                                if cCurve.isClose then bNeibourBone = (getObjWithName cCurve.boneLst[1] exact:True)[1]
                                else bNeibourBone = undefined
                            )
                        )
                        boneIndex += 1
                    )
                    if foundBone then exit
                )
            )

--             sm_addPosCons smBone fNeibourBone bNeibourBone

            sm_addAimCons smBone smBoneRefLoc smBonePreLoc smBoneUpLoc spring_bend tension isTension -- fNeibourBone bNeibourBone

            sm_addOriCons smBoneUpLoc smBonePreUpLoc smBoneRefUpLoc spring_twist

            oldRange = animationRange
            if startFrame >= 0 then(
                animationRange = interval -1 (endFrame + 1)
            )
            else(
                animationRange = interval (startFrame-1) (endFrame + 1)
            )
            with animate on(
                -- disable constraint before and after cal range
                sliderTime = 0
                smBone.rotation.controller.weight = #(100.0, 0.0)
                sliderTime = (startFrame - 1)
                smBone.rotation.controller.weight = #(100.0, 0.0)
                sliderTime = startFrame
                smBone.rotation.controller.weight = #(0.0, 100.0)
                sliderTime = endFrame
                smBone.rotation.controller.weight = #(0.0, 100.0)
                sliderTime = (endFrame + 1)
                smBone.rotation.controller.weight = #(100.0, 0.0)
            )
            animationRange = oldRange

            -- add pre TM attr
            local bonePrePos_attr = attributes bonePrePos_attr (parameters main (bonePrePos type:#point3))
            custAttributes.add smBone bonePrePos_attr
            local boneRefPos_attr = attributes boneRefPos_attr (parameters main (boneRefPos type:#point3))
            custAttributes.add smBone boneRefPos_attr
            local bonePreUpTM_attr = attributes bonePreUpTM_attr (parameters main (bonePreUpTM type:#matrix3))
            custAttributes.add smBone bonePreUpTM_attr
            local boneLength_attr = attributes boneLength_attr (parameters main (boneLength type:#float))
            custAttributes.add smBone boneLength_attr
--             local boneCurLengthAttr = attributes boneCurLengthAttr (parameters main (boneCurLength type:#float))
--             custAttributes.add smBone boneCurLengthAttr            
            local boneChildRefPos_attr = attributes boneChildRefPos_attr (parameters main (boneChildRefPos type:#point3))
            custAttributes.add smBone boneChildRefPos_attr
            local isCollide_attr = attributes isCollide_attr (parameters main (isCollide type:#boolean))
            custAttributes.add smBone isCollide_attr
            local isCollidePlane_attr = attributes isCollidePlane_attr (parameters main (isCollidePlane type:#boolean))
            custAttributes.add smBone isCollidePlane_attr            
            local boneChildRefTM_attr = attributes boneChildRefTM_attr (parameters main (boneChildRefTM type:#matrix3))
            custAttributes.add smBone boneChildRefTM_attr
            local bonePreLocPos_attr = attributes bonePreLocPos_attr (parameters main (bonePreLocPos type:#point3))
            custAttributes.add smBone bonePreLocPos_attr

            if isTargetFrame then sliderTime = targetFrame
            else sliderTime = startFrame

            smBoneChild = smBone.children[1]

            smBone.boneRefPos = smBoneRefLoc.pos.controller.value
            smBone.boneLength = smBoneChild.pos.controller.value[1]
            smBone.bonePreLocPos = [0,0,0]

            if smBoneChildRefLoc != undefined then(
                if (findItem boneLst smBoneChild)>0 then (
                    smBoneChild.isCollide = False
                    smBoneChild.isCollidePlane = False
                )
                boneGrandChild = smBoneChild.children[1]
                if boneGrandChild != undefined then(
                    smBoneChildRefLoc.pos = boneGrandChild.pos
                    smBone.boneChildRefPos = smBoneChildRefLoc.pos.controller.value
                )
            )
            sliderTime = (startFrame-1)
            with animate on (
                smBoneChild.Pos = smBoneChild.Pos
                smBone.bonePrePos = smBoneChild.Pos
                smBone.bonePreUpTM = smBone.transform
            )
--             point pos:smBone.bonePrePos
            sliderTime = startFrame
        )
    )
    -- attr list added by script that gona remove later
    attrNameLst = #(\
        "bonePrePos_attr" as name,\
        "boneRefPos_attr" as name,\
        "bonePreUpTM_attr" as name,\
        "boneLength_attr" as name,\
        "boneChildRefPos_attr" as name,\
        "isCollide_attr" as name,\
        "isCollidePlane_attr" as name,\
        "bonePreLocPos_attr" as name,\
        "boneChildRefTM_attr" as name
    )


    i = startFrame
    sliderTime = startFrame
    -- initial capsule pq
    if isCollision and (colBodyLst != undefined) then(
        for colBody in colBodyLst do(
            pq = getColCapsulePQ colBody
            colBody.curPosP = pq[1]
            colBody.curPosQ = pq[2]
            colBody.isFastMove = False
        )
    )
    pre_i = startFrame
    while i < (endFrame+(1/subDiv)) do (
        with animate on (
            at time i(  -- frame loop
--                 print "------------------------------"
--                 print i
                for smBone in boneLst do(   -- bone loop
                    if smBone.children.count>0 then(
                        smBoneChild = smBone.children[1]
                        smBoneRefLoc = sm_getLoc smBone aimRefLoc_subFix
                        smBonePreLoc = sm_getLoc smBone aimPreLoc_subFix
                        smBoneCurLocBase = sm_getLoc smBone aimCurLocBase_subFix
                        smBoneCurLoc = sm_getLoc smBone aimCurLoc_subFix
                        smBoneUpLoc = sm_getLoc smBone aimUpLoc_subFix
                        smBonePreUpLoc = sm_getLoc smBone aimPreUpLoc_subFix
                        smBoneChildRefLoc = sm_getLoc smBone aimChildRefLoc_subFix

                        if i == startFrame and (not isFirstPass) then (
                            -- paste last frame pose to first fram for loop
                            sm_pasteBoneTM boneLst
                        )

                        smBoneRefLoc.pos.controller.value = smBone.boneRefPos
                        smBonePreLoc.pos = smBone.bonePrePos

                        -- apply wind
                        if isWind then(
                            sm_applyForce smBoneRefLoc windObj subDiv i False
                        )

                        -- apply explosion
                        if isExp then(
                            sm_applyForce smBoneRefLoc expObj subDiv i True
                        )

                        -- detect collision
                        col_pre = undefined
                        col_cur = undefined
                        -- apply capsule collision
                        if isCollision and (colBodyLst != undefined) then(
                            preResult = preCheckCollision smBone colBodyLst
                            if preResult != undefined then(
                                -- check col from previous pos to cur pos
                                checkResult = checkCollision smBoneRefLoc.pos smBonePreLoc.pos colBodyLst True fastMoveNum subDiv
                                col_pre = checkResult[1]
                                col_body_pre = checkResult[2]
                                -- check col from cur pos to previous pos
                                checkResult = checkCollision smBoneRefLoc.pos smBonePreLoc.pos colBodyLst False fastMoveNum subDiv
                                col_cur = checkResult[1]
                                col_body_cur = checkResult[2]
                                -- debug
    --                             if (col_pre != undefined) then(
    --                                 point name:("col_pre Frame "+i as string) pos:col_pre size:3
    --                             )
    --                             if (col_cur != undefined) then(
    --                                 point name:("col_cur Frame "+i as string) pos:col_cur size:3
    --                             )

                                if (col_pre != undefined) and (col_cur == undefined) then(
                                    smBoneRefLoc.pos = col_pre
                                )
                                if (col_cur != undefined) and (col_pre == undefined) then(
                                    smBonePreLoc.pos = col_cur
                                )
                                if (col_pre != undefined) and (col_cur != undefined) then(
                                    -- move cur child pose to closest out point if both pre and cur pos are already inside of col body
                                    midPoint = (smBonePreLoc.pos+smBoneRefLoc.pos)/2
                                    if (sm_distance col_pre midPoint) < (sm_distance col_cur midPoint) then(
                                        smBoneRefLoc.pos = col_pre
--                                         print i
--                                         print col_body_pre.isFastMove
                                        if col_body_pre.isFastMove then(
                                            moveRate = (col_body_pre.moveSpeed-fastMoveNum)/(fastMoveNum+0.01)
                                            if moveRate>1.0 then moveRate = 1.0
                                            if moveRate<0.0 then moveRate = 0.0
                                            smBonePreLoc.pos = col_pre*moveRate+smBonePreLoc.pos*(1-moveRate)
                                        )
                                    )
                                    else(
                                        smBoneRefLoc.pos = col_cur
--                                         print i
--                                         print col_body_cur.isFastMove
                                        if col_body_cur.isFastMove then(
                                            moveRate = (col_body_cur.moveSpeed-fastMoveNum)/(fastMoveNum+0.01)
                                            if moveRate>1.0 then moveRate = 1.0
                                            if moveRate<0.0 then moveRate = 0.0
                                            smBonePreLoc.pos = col_cur*moveRate+smBonePreLoc.pos*(1-moveRate)
                                        )
                                    )
                                )
                            )
                        )

                        -- apply plane collision
                        isPlaneHit = False
                        if isCollision and (colPlaneLst != undefined) then(
                            colPlane = colPlaneLst[1]
                            colResult_Plane = checkPlaneCollision smBone smBoneRefLoc colPlane

                            isCollidePlane_Parent = False
                            if (findItem boneLst smBone.parent)>0 then(
                                isCollidePlane_Parent = smBone.parent.isCollidePlane
                            )

                            if colResult_Plane != undefined or isCollidePlane_Parent then(
                                sm_repeatMoveToPlane smBoneRefLoc smBoneChild colPlane 3
                                isPlaneHit = colResult_Plane != undefined
                            )
                        )

                        isHit = (col_pre != undefined) or (col_cur != undefined) or isPlaneHit

                        -- update tension locator pos
                        if smBoneChildRefLoc != undefined then(
                            if (findItem boneLst smBoneChild)>0 then(
                                boneGrandChild = smBoneChild.children[1]
                                if boneGrandChild != undefined then(
                                    smBoneChildRefLoc.pos = boneGrandChild.pos
                                )
                                -- if clide, set parent tension on
                                if (findItem boneLst smBone.parent)>0 then(
                                    if isHit then(
                                        aimCons = smBone.parent.rotation.controller[2]
                                        aimWeightLst = aimCons.weight
                                        if aimWeightLst.count>2 then(
                                            aimWeightLst[3] = tension
                                        )
                                    )
                                    else(
                                        aimCons = smBone.parent.rotation.controller[2]
                                        aimWeightLst = aimCons.weight
                                        if aimWeightLst.count>2 then(
                                            aimWeightLst[3] = 0.0
                                        )
                                    )
                                )
                            )
                        )

                        -- flex
                        if extend != 0.0 then(
--                             x2 = (sm_distance smBone.pos smBonePreLoc.pos)
--                             x3 = smBone.boneLength*(1-extend) + x2*extend
--                             smBoneChild.pos.controller.value.x = x3 
                            -- get length between bone pos and previous child pos
                            if subDiv>1 then(
                                if ceil pre_i.frame == floor i.frame then(    -- pass through an int frame number
                                    -- update bonePreLocPos on int frame
                                    with animate off(
                                        smBone.bonePreLocPos = smBonePreLoc.pos
                                    )
                                )
                            )
                            else(
                                with animate off(
                                    smBone.bonePreLocPos = smBonePreLoc.pos
                                )
                            )
                            x2 = (sm_distance smBone.pos smBone.bonePreLocPos)
                            x3 = smBone.boneLength*(1-extend) + x2*extend
                            smBoneChild.pos.controller.value.x = x3

                            pre_i = i

                        )

                        -- twist
                        smBonePreUpLoc.transform = smBone.bonePreUpTM
                        with animate off(
                            smBone.bonePrePos = smBoneChild.pos
                            smBone.bonePreUpTM = smBoneUpLoc.transform
                            smBone.isCollide = (col_pre != undefined) or (col_cur != undefined)
                            smBone.isCollidePlane = isPlaneHit
                        )
                    )
                ) -- bone loop end
                -- update col body pq
                with animate off(
                    if isCollision and (colBodyLst != undefined) then(
                        for colBody in colBodyLst do(
                            pq = getColCapsulePQ colBody
                            colBody.prePosP = pq[1]
                            colBody.prePosQ = pq[2]
                        )
                    )
                )
            )
        )
        if i == endFrame and isLoop and isFirstPass then(
            sm_copyBoneTM boneLst
            i = startFrame
            isFirstPass = False
        )
        else(
            i += 1/subDiv
        )
--         sm_SpringMax.springProgress.value = i as integer
        ppp = 0
        qqq = 1
        if isLoop then(
            qqq = 2
            if not isFirstPass then(
                ppp = 40
            )
        )
        progressUpdate(((i-startFrame) as float/(endFrame-startFrame) as float)*80/qqq+ppp)
        -- sm_SpringMax.springProgress.value = ((i-startFrame) as integer/(endFrame-startFrame) as integer)*100
    ) -- frame loop end


    -- collapse bone result
    if isWipe then subDiv = 1.0
    i = 1.0
    for smBone in boneLst do(
        collapseTransform smBone startFrame endFrame subDiv isTcb
        -- remove custom attr made by script
        boneCusAttrLst = custAttributes.getDefs smBone
        if boneCusAttrLst != undefined then(
            for attr in boneCusAttrLst do(
                if (findItem attrNameLst attr.name)>0 then(
                    custAttributes.delete smBone attr
                )
            )
        )
        progressUpdate(i/boneLst.count*15+80)
        i+=1
    )
    progressUpdate 95
    -- remove locators
    for item in boneLocLst do delete item


    progressUpdate 100
)

fn sm_straightBone =(
    for obj in $Selection do(
        obj.rotation.controller.value = (quat 0 0 0 1)
    )
)

fn sm_initBoneTM boneLst =(
    for obj in boneLst do(
        try(
            obj.pos.controller.value = obj.boneInitPos
            obj.rotation.controller.value = execute obj.boneInitRot
        )
        catch()
    )
)

fn RotatePivotOnly obj rotation= (
    local rotValInv=inverse (rotation as quat)
    animate off in coordsys local obj.rotation*=RotValInv
    obj.objectoffsetrot*=RotValInv
    obj.objectoffsetpos*=RotValInv
)

fn createColCapsule obj =(
    colCapsule = Capsule radius:3 height:10 sides:8 heighttype:0
    colCapsule.xray = on
    colCapsule.renderable = off
    -- fix capsule axis to follow x
    RotatePivotOnly colCapsule (quat 0 1 0 1)
    -- lock scale
    setTransformLockFlags colCapsule #{7,8,9}

    if obj != undefined and (ClassOf obj == Biped_Object or ClassOf obj == BoneGeometry) then(
        colCapsule.name = obj.name+colBody_subFix
        colCapsule.transform = obj.transform
        colCapsule.parent = obj
        if obj.children.count > 0 then(
            if ClassOf obj == Biped_Object then(
                childTX = (obj.children[1].transform * (inverse obj.transform)).pos.x
                bipedSize = biped.getTransform obj #scale
                colCapsule.radius = (bipedSize.y+bipedSize.z)/20
            )
            if ClassOf obj == BoneGeometry then(
                childTX = obj.children[1].pos.controller.value.x
                boneSize = (obj.width+obj.height)/2
                colCapsule.radius = boneSize
            )
            colCapsule.height = childTX+2*colCapsule.radius
            colCapsule.pos.controller.value = [-colCapsule.radius,0,0]
        )
    )
    else(
        colCapsule.name = "the"+colBody_subFix
    )

    local prePosP_attr = attributes prePosP_attr (parameters main (prePosP type:#point3))
    custAttributes.add colCapsule prePosP_attr
    local prePosQ_attr = attributes prePosQ_attr (parameters main (prePosQ type:#point3))
    custAttributes.add colCapsule prePosQ_attr
    local curPosP_attr = attributes curPosP_attr (parameters main (curPosP type:#point3))
    custAttributes.add colCapsule curPosP_attr
    local curPosQ_attr = attributes curPosQ_attr (parameters main (curPosQ type:#point3))
    custAttributes.add colCapsule curPosQ_attr
    local isFastMove_attr = attributes isFastMove_attr (parameters main (isFastMove type:#boolean))
    custAttributes.add colCapsule isFastMove_attr
    local moveSpeed_attr = attributes moveSpeed_attr (parameters main (moveSpeed type:#float))
    custAttributes.add colCapsule moveSpeed_attr

    colCapsule
)


fn sm_createColPlane =(

    -- remove exist wind cone
    if (getObjWithName colPlane_subFix) != undefined then(
        delete (getObjWithName colPlane_subFix)[1]
    )
    colPlane = plane name:("the"+colPlane_subFix)
    colPlane.xray = on
    colPlane.renderable = off
    colPlane.backfacecull = on
    colPlane.lengthsegs = 1
    colPlane.widthsegs = 1
    colPlane.length = 50.0
    colPlane.width = 50.0
    -- lock scale
    setTransformLockFlags colPlane #{7,8,9}

    select colPlane
)



-------------------------------
-- UI
-------------------------------
floaterWidth = 388
floaterHeight = 480
paypalLink = @"https://www.paypal.me/Yanbin/10usd"
zhTutorialsLink = @"https://animbai.com/zh/2018/08/18/spring-max-tutorial/"
enTutorialsLink = @"https://animbai.com/2018/08/18/spring-max-tutorial/"
downloadLink = @"https://animbai.com/2018/08/12/spring-max/"
linkedLink = @"https://www.linkedin.com/in/baiyanbin/"
versionCheckLink = @"http://animbai.com/springmaxver/"

scriptPath = getFileNamePath(getThisScriptFilename())
iconPath = scriptPath + @"springMax_icons\"
-- iniFile = iconPath + "springMax.ini"
-- iniLan = GetINISetting iniFile "Basic" "Lan"
uistrLst = #()
spamWord = #()
spamWordCHN = #()

sm_SpringMax = undefined
fn springMax_Launcher =(
    -- get select bones, ignore root bone
    boneLst = for obj in $Selection where (ClassOf obj == BoneGeometry) and (obj.parent!=undefined) collect obj
    if boneLst.count>0 then(
        if sm_SpringMax.springKeyRange_radio.state == 1 then(
            startFrame = animationRange.start
            endFrame = animationRange.end
        )
        else(
            startFrame = sm_SpringMax.springTimeFrom_spinner.value as time
            endFrame = sm_SpringMax.springTimeTo_spinner.value as time
        )
        if sm_SpringMax.springColActive_checkbox.state then(
            subDiv = sm_SpringMax.springTimeSubFrame_spinner.value as float
        )
        else(
            subDiv = 1.0
        )
        spring_bend = 1 - sm_SpringMax.springBend_spinner.value
        spring_twist = 1 - sm_SpringMax.springTwist_spinner.value
        extend = sm_SpringMax.springFlex_spinner.value
        tension = sm_SpringMax.springTension_spinner.value
        isWipe = sm_SpringMax.springTimeWipe_checkbox.state
        isLoop = sm_SpringMax.springTimeLoop_checkbox.state
        isTcb = sm_SpringMax.springTcb_checkbox.state
        isCollision = sm_SpringMax.springColActive_checkbox.state
        isFastMove = sm_SpringMax.springColFast_checkbox.state
        fastMoveNum = sm_SpringMax.springColFastNum_spinner.value
        colBodyLst = getObjWithName colBody_subFix
        colPlaneLst = getObjWithName colPlane_subFix

        isWind=False
        windObj=undefined
        if (getObjWithName windCone_subFix) != undefined then(
            isWind = True
            windObj = (getObjWithName windCone_subFix)[1]
        )

        isExp=False
        expObj=undefined
        if (getObjWithName expCtrl_subFix) != undefined then(
            isExp = True
            expObj = (getObjWithName expCtrl_subFix)[1]
        )

        isTargetFrame = sm_SpringMax.springIsTargetFrame_checkbox.state
        targetFrame = sm_SpringMax.springTargetFrame_spinner.value

        slidertime = startFrame
        -- restore bone init pose before calculation
        sm_restoreInitPose boneLst

        with redraw off(

            progressStart uistrLst[2][2]
            springMax_Main boneLst spring_bend spring_twist extend tension startFrame endFrame isTargetFrame targetFrame subDiv isWipe isLoop isTcb isCollision isFastMove fastMoveNum colBodyLst colPlaneLst isWind windObj isExp expObj
            progressEnd()
        )
        gc()
    )
)

lang=sysinfo.getmaxlanguage()
uistr="ENG"
try
(
    if lang[3]=="CHS" then uistr="CHN"
    -- from max 2013 there has a default language setting
    if (getINISetting (getMAXIniFile()) "File Language Options" "LanguageToUseForFileIO") == "2052" then uistr="CHN"
)
catch(uistr="ENG")

uistrLst_CHN = #(\
                #(1,"Spring Max","                    白严宾","- 施工中 -"),\
                #(2,"飘带计算:","算飘带"," 名称重复, 退出计算!"," 丢失, 退出计算!"),\
                #(3,"打赏","需要你的支持!","Spring Max 是一款免费工具, 你可以无偿使用它的所有功能! 然而, 如果它使你的工作轻松了一些的话...","                                 请我喝杯奶茶怎么样？"),\
                #(4,"摆动:","飘带摆动幅度，数值越大结果越柔软"),\
                #(5,"拧转:","拧转效果（X轴）的柔软度，数值越大拧得越多"),\
                #(6,"伸缩:","给骨骼链增加弹性，以产生拉伸和挤压的效果"),\
                #(7,"张力:","链条张力，用于定义由碰撞造成的弯曲如何在链条中传递，仅在碰撞发生时生效。\n适当设置可减少碰撞计算结果的抖动和穿插。"),\
                #(8,"关键帧设置"),\
                #(9,"当前        ","","从:","到:"),\
                #(10,"循环","计算两次，以获得循环动画"),\
                #(11,"拆帧:","将一帧拆分成若干碎帧来进行细分计算，以捕捉非常快速移动的碰撞体\n如设为5，则会以1/5帧为步进来计算\n相应的会成倍增加计算时间，但能获得更精细的效果"),\
                #(12,"清理碎帧","在计算完成后清理不在整数上的细碎关键帧"),\
                #(13,"TCB","将计算结果转换为TCB旋转，可以减少链条在两帧之间抖动"),\
                #(14,"碰撞设置"),\
                #(15,"启用","勾选后会计算飘带与场景中胶囊体或平面的碰撞"),\
                #(16,"快速移动","如果胶囊体附加在快速移动的物体上，比如奔跑中的双腿与裙子的碰撞。勾选此项时，可以减少穿插。\n注意：在碰撞体移动较慢时勾选此项，可能会造成结果抖动。"),\
                #(17,"+ 胶囊","创建一个碰撞胶囊体，如果不选择骨骼，则在原点生成，如选择骨骼，则跟骨骼大小位置对齐\n注意：请勿通过缩放来调整胶囊大小，也不要改变名称，否则可能造成计算错误"),\
                #(18,"+ 平面","创建一个碰撞平面\n注意：请勿改变名称"),\
                #(19,"清理","清除所有碰撞物体"),\
                #(20,"骨骼姿态"),\
                #(21,"起始","粘贴回计算之前的起始帧姿态"),\
                #(22,"拉直","将骨骼链拉直"),\
                #(23,"复制","复制当前姿态"),\
                #(24,"粘贴","粘帖复制的姿态"),\
                #(25,"高级"),\
                #(26,"关联","关联选中的骨骼，使其能在计算时相互影响","闭环"),\
                #(27,"拾取","创建拾取控制器来拎起骨骼"),\
                #(28,"外力"),\
                #(29,"风","创建一个风力物体"),\
                #(30,"爆炸","创建一个爆炸物体"),\
                #(31,"计 算","开始计算选中骨骼"),\
                #(32,"有新版本！","B站中文教程","YouTube英文教程","界面语言","作者简介","工具栏命令"),\
                #(33,"指定目标帧:","指定一个关键帧，使骨骼计算结果向其靠拢\n如不指定，则默认向起始帧靠拢\n该功能主要是为了解决从动画中间开始计算时，骨骼仍能正确向最终姿势靠拢","指定目标帧"),\
                #(34,"灵敏度","碰撞体速度达到多少时，会触发骨骼全力追赶它\n数值越小，灵敏度越高，但也可能造成更多的抖动"),\
                #(35,"更多内容，请访问我的网站:","animbai.com"),\
                #()\
                )
uistrLst_ENG = #(\
                #(1,"Spring Max","                Bai Yanbin","  - WIP -"),\
                #(2,"Spring Calculating:","Spring Max"," has duplication name, quit calculation!"," missing, quit calculation!"),\
                #(3,"Donate","Need Your Supports!","Spring Max is free for everyone, which means you can use FULL FUNCTION as FREE! However if this tool make your life easier...","                   HOW about bought me a cup of coffee?"),\
                #(4,"Spring:","How soft the spring is, bigger value, softer result."),\
                #(5,"Twist:","How soft the twist (X-axis) is."),\
                #(6,"Flex:","Flexibility of spring, which can produce strach and squash animation."),\
                #(7,"Tension:","Tension of the chain, means how much the force of bend will be path through the chain, only take effect when collision happends.\nCan reduce poping and clip through of collision result."),\
                #(8,"Key Setting"),\
                #(9,"Active   ","","From","To"),\
                #(10,"Loop","Will caculate twice to get looped result."),\
                #(11,"Sub:","How many sub frame that will be go through during 1 frame, that for capture really fast move collision body.\nIf set as 5, means will step as 1/5 frame to calculation.\nWill increase calculation time by times"),\
                #(12,"Wipe Sub","Remove precision keys on sub frame after calculation."),\
                #(13,"TCB","Bake rotation as TCB. Can reduce jittery in-between frame."),\
                #(14,"Collision Setting"),\
                #(15,"Active","Active collision calculation with capsule body or plane in scene."),\
                #(16,"Fast Move","Click on this if capsule attach on a fast move bone, like sprinting legs, to reduce the clip through.\nNote: May cause more poping if capsule moving slowly."),\
                #(17,"+ Body","Add a collision capsule at original point if select notion, or add a capsule match with selected bone.\nDO NOT change capsule shape with scale function and change name, otherwise may cause wrong result."),\
                #(18,"+ Plane","Add a collision plane at original point\nDO NOT change name"),\
                #(19,"Clear","Remove all collison object in scene"),\
                #(20,"Bone Pose"),\
                #(21,"Init Pose","Back to start frame pose which BEFORE the calculation."),\
                #(22,"Straight","Set joint chain as straight"),\
                #(23,"Copy","Copy current bone poses"),\
                #(24,"Paste","Paste copied poses"),\
                #(25,"Advance"),\
                #(26,"Connect","Connect selected bone that make them can inteact each other","Close"),\
                #(27,"Pick","Create a pick controller to lift up bone chain"),\
                #(28,"Force"),\
                #(29,"Wind","Create a wind object"),\
                #(30,"Explosion","Create a explotion object"),\
                #(31,"Apply","Start calculate selected bones."),\
                #(32,"New Version Available!","Tutorials on bilibili","Tutorials on YouTube","Set Language","Author Profile","Toolbar Command"),\
                #(33,"Target Frame:","Assign a frame as reference that calculation should move towards.\nIf set False, then will use start frame as target.\nCan use this let bone chain still back to correct pose if start calculation from middle of animation.","Target Frame"),\
                #(34,"Sensitivity","How fast the collision body moves that will trigger bone trying to catch it as close as possiable.\nSmaller the number is more sensitive the bone will be, more jittery may cause as well."),\
                #(35,"To know more, check my site:","animbai.com"),\
                #()\
                )
if uistr=="CHN" then uistrLst=uistrLst_CHN else uistrLst=uistrLst_ENG
linkColor = (color 0 250 200)
rollout sm_Donate uistrLst[3][2] width:376 height:284
(
    groupBox sm_grp1 uistrLst[3][3] pos:[5,13] width:364 height:255
    label sm_lbl1 uistrLst[3][4] pos:[32,41] width:312 height:39
    label sm_lbl2 uistrLst[3][5] pos:[32,77] width:312 height:24
    bitmap sm_alipay_pic "alipay_pic" pos:[14,108] width:120 height:120 fileName:(iconPath+"ali_pay.png")
    bitmap sm_Wechat_pic "Wechat_pic" pos:[240,108] width:120 height:120 fileName:(iconPath+"wechat_pay.png")
    button sm_paypal_btn "paypal" pos:[156,140] width:64 height:64 images:#(iconPath+"paypal.png", undefined, 1,1,1,1,1,True) iconSize:[64,64]
    label sm_lbl3 uistrLst[35][2] pos:[75,240] width:312 height:24
    hyperLink web uistrLst[35][3] pos:[235,240] color:linkColor hoverColor:linkColor visitedColor:linkColor address:"http://animbai.com/"

    on sm_paypal_btn pressed  do(
        shellLaunch paypalLink ""
    )
)


rollout sm_SpringMax uistrLst[2][3] width:376 height:404
(
    spinner springBend_spinner uistrLst[4][2] toolTip:uistrLst[4][3] pos:[42,16] width:52 height:16 range:[0,1,0.7] scale:0.1
    spinner springTwist_spinner uistrLst[5][2] toolTip:uistrLst[5][3] pos:[128,16] width:52 height:16 range:[0,1,0.7] scale:0.1
    spinner springFlex_spinner uistrLst[6][2] toolTip:uistrLst[6][3] pos:[212,16] width:52 height:16 range:[0,1,0] scale:0.1
    spinner springTension_spinner uistrLst[7][2] toolTip:uistrLst[7][3] pos:[306,16] width:52 height:16 range:[0,1,0.5] scale:0.1 enabled:false
    groupBox grp1 uistrLst[8][2] pos:[13,47] width:348 height:81
    radioButtons springKeyRange_radio "" pos:[45,72] width:125 height:16 labels:#(uistrLst[9][2], uistrLst[9][3]) columns:2
    spinner springTimeFrom_spinner uistrLst[9][4] pos:[150,72] width:64 height:16 enabled:false range:[-99999,99999,0] type:#integer scale:1
    spinner springTimeTo_spinner uistrLst[9][5] pos:[234,72] width:64 height:16 enabled:false range:[-99999,99999,1] type:#integer scale:1
    checkbox springTimeLoop_checkbox uistrLst[10][2] toolTip:uistrLst[10][3] pos:[310,72] width:46 height:16 enabled:true checked:false
    spinner springTimeSubFrame_spinner uistrLst[11][2] toolTip:uistrLst[11][3] pos:[40,102] width:52 height:16 enabled:false range:[1,100,1] type:#integer scale:1
    checkbox springTimeWipe_checkbox uistrLst[12][2] toolTip:uistrLst[12][3] pos:[102,102] width:70 height:16 enabled:false checked:true
    checkbox springTcb_checkbox uistrLst[13][2] toolTip:uistrLst[13][3] pos:[310,102] width:46 height:16 enabled:true checked:true
    groupBox grp2 uistrLst[14][2] pos:[16,144] width:344 height:64
    checkbox springColActive_checkbox uistrLst[15][2] toolTip:uistrLst[15][3] pos:[30,172] width:50 height:16 enabled:true checked:false
    checkbox springColFast_checkbox uistrLst[16][2] toolTip:uistrLst[16][3] pos:[90,172] width:70 height:16 enabled:false checked:true visible:False
    spinner springColFastNum_spinner uistrLst[34][2] toolTip:uistrLst[34][3] pos:[120,172] width:70 height:16 range:[0,100,1.5] scale:0.1 enabled:false
    button springColAddBody_btn uistrLst[17][2] toolTip:uistrLst[17][3] pos:[200,160] width:40 height:40 images:#(iconPath+"addCapsule.png", iconPath+"addCapsule_a.png", 1,1,1,1,1,True) border:False
    button springColAddPlane_btn uistrLst[18][2] toolTip:uistrLst[18][3] pos:[250,160] width:40 height:40 images:#(iconPath+"addPlane.png", iconPath+"addPlane_a.png", 1,1,1,1,1,True) border:False
    button springColClear_btn uistrLst[19][2] toolTip:uistrLst[19][3] pos:[300,160] width:40 height:40 images:#(iconPath+"clearCapsule.png", iconPath+"clearCapsule_a.png", 1,1,1,1,1,True) border:False
    groupBox grp3 uistrLst[20][2] pos:[16,224] width:128 height:112
    button springBoneInit_btn uistrLst[21][2] toolTip:uistrLst[21][3] pos:[24,248] width:54 height:32
    button springBoneStraight_btn uistrLst[22][2] toolTip:uistrLst[22][3] pos:[24,294] width:54 height:32
    button springBoneCopy_btn uistrLst[23][2] toolTip:uistrLst[23][3] pos:[82,248] width:54 height:32
    button springBonePaste_btn uistrLst[24][2] toolTip:uistrLst[24][3] pos:[82,294] width:54 height:32
    groupBox grp4 uistrLst[25][2] pos:[152,224] width:96 height:112
    label springTmp_lb uistrLst[1][4] pos:[175,270] width:50 height:24
    checkbox springIsTargetFrame_checkbox pos:[175,102] width:10 height:16 enabled:true checked:false
    spinner springTargetFrame_spinner uistrLst[33][2] toolTip:uistrLst[33][3] pos:[248,102] width:52 height:16 range:[-99999,99999,0] type:#integer scale:1 enabled:False
    button springConnect_btn uistrLst[26][2] toolTip:uistrLst[26][3] pos:[154,248] width:32 height:32 images:#(iconPath+"connect.png", undefined, 1,1,1,1,1,False) border:False visible:False
    checkbox springConnectClose_checkbox uistrLst[26][4] pos:[192,256] width:44 height:16 visible:False
--     button springPick_btn uistrLst[27][2] toolTip:uistrLst[27][3] pos:[162,293] width:54 height:32 images:#(iconPath+"ctrl_bake.png", undefined, 1,1,1,1,1,False)
--     label springCtrlBake_lb "Bake" pos:[204,300] width:32 height:32
    groupBox grp5 uistrLst[28][2] pos:[256,224] width:104 height:64
    button springWind_btn uistrLst[29][2] toolTip:uistrLst[29][3] pos:[264,248] width:36 height:36 images:#(iconPath+"wind.png", iconPath+"wind_a.png", 1,1,1,1,1,True) border:False
--     label springCtrlWind_lb "Wind" pos:[290,225] width:32 height:32
    button springExp_btn uistrLst[30][2] toolTip:uistrLst[30][3] pos:[318,248] width:32 height:32 images:#(iconPath+"expWind.png", iconPath+"expWind_a.png", 1,1,1,1,1,True) border:False
--     label springCtrlExp_lb "Explosion" pos:[290,260] width:64 height:32
    button springApply_btn uistrLst[31][2] toolTip:uistrLst[31][3] pos:[260,294] width:98 height:42  border:True
    button springMiscUpdate_btn "update" toolTip:uistrLst[32][2] pos:[16,374] width:25 height:25 images:#(iconPath+"update.png", iconPath+"update_a.png", 1,1,1,1,1,True) visible:False border:False
    button springMiscBili_btn "bili" toolTip:uistrLst[32][3] pos:[120,354] width:40 height:40 images:#(iconPath+"bilibili.png", undefined, 1,1,1,1,1,False) border:False
    button springMiscYoutu_btn "You" toolTip:uistrLst[32][4] pos:[168,354] width:40 height:40 images:#(iconPath+"youtube.png", undefined, 1,1,1,1,1,True) border:False
    button springMiscLan_btn "Lan" toolTip:uistrLst[32][5] pos:[264,354] width:40 height:40 images:#(iconPath+"language.png", undefined, 1,1,1,1,1,True) border:False
    button springMiscLink_btn "Link" toolTip:uistrLst[32][6] pos:[216,354] width:40 height:40 images:#(iconPath+"linkedin.png", undefined, 1,1,1,1,1,True) border:False
    button springMiscShelf_btn "Shelf" toolTip:uistrLst[32][7] pos:[312,354] width:40 height:40 images:#(iconPath+"Shelf.png", undefined, 1,1,1,1,1,True) border:False
--     progressBar springProgress "" pos:[16,404] width:344 height:24
--     label springProgress_lb "0%" pos:[182,408]
    button spingLanCHN_btn "CHN" pos:[266,282] width:36 height:36 images:#(iconPath+"Chinese.png", undefined, 1,1,1,1,1,True) visible:False
    button spingLanENG_btn "ENG" pos:[266,318] width:36 height:36 images:#(iconPath+"english.png", undefined, 1,1,1,1,1,True) visible:False
    label springSpam_lb "" pos:[120,400] width:312 height:24

    fn sm_setLan lanName =(
--         SetINISetting iniFile "Basic" "Lan" lanName
        if lanName == "ENG" then(
            lanCode = "1033"
        )
        else(
            lanCode = "2052"
        )
        SetINISetting (getMAXIniFile()) "File Language Options" "LanguageToUseForFileIO" lanCode
        try(closeRolloutFloater springMaxWindow)catch()
        execute("fileIn (@\""+(getThisScriptFilename())+"\")")
    )

    fn sm_updateTimeRange =(
        animationRange = interval springTimeFrom_spinner.value springTimeTo_spinner.value
    )

    fn sm_toggleLanUI =(
        spingLanCHN_btn.visible = not spingLanCHN_btn.visible
        spingLanENG_btn.visible = not spingLanENG_btn.visible
        springApply_btn.visible = not springApply_btn.visible
    )

    fn readWebPage urlLink timeOut =(
        requestClass = dotNetClass "System.Net.WebRequest"
        request = requestClass.Create(urlLink)
        request.Method = "GET"
        request.Timeout = timeOut

        response = request.getResponse()
        -- response is an HttpWebResponse
        responseStream = response.GetResponseStream()

        encodingClass = dotNetClass "System.Text.Encoding"
        encoding = encodingClass.GetEncoding "utf-8"

        readStream = dotNetObject "System.IO.StreamReader" responseStream encoding
        pageData = readStream.ReadToEnd()
        readStream.Close()
        response.Close()

        pageData
    )

    fn getPageWord pageData keyWord =(
        skipToString pageData keyWord
        a = readLine pageData errorAtEOF:false
--         print a
--         b = (filterString a keyWord)[1]
--         print b
        c = findString a keyWord
        substring a 1 (c-1)
    )

--     fn getNewVersion = (
--         try(
--             pageData = (readWebPage versionCheckLink 5000) as stringStream 
--         )
--         catch()
--         if pageData != undefined then(
-- --             print pageData
-- --             findString pageData "ver"
-- --             a = (filterString pageData "#####ver#####")[2]
-- --             b = (filterString a "#####ver#####")[1]
-- --             print b
--             b = getPageWord pageData "|SpringMax|"
--             (b as float)
--         )
--         else 0.0
--     )

    on sm_SpringMax open do(
        version = 0.0
        try(
            pageData = (readWebPage versionCheckLink 5000) as stringStream 
        )
        catch()
        if pageData != undefined then(
            version = (getPageWord pageData "|SpringMax|") as float
            for j = 1 to 5 do(
                spamKeyWord = "|spam"+(j as string)+"|"
                append spamWord (getPageWord pageData spamKeyWord)
            )
            for j = 1 to 5 do(
                spamKeyWordCHN = "|spam"+(j as string)+"chn|"
                append spamWordCHN (getPageWord pageData spamKeyWordCHN)
            )
            -- set spam word
            if uistr=="CHN" then(
                springSpam_lb.caption = spamWordCHN[(random 1 5)]
            )
            else springSpam_lb.caption = spamWord[(random 1 5)]
        )
        else print "Fail to load page!"

        if version > sm_ver then(
            springMiscUpdate_btn.visible = True
        )

    )

    on springKeyRange_radio changed state do(
        a = springKeyRange_radio.state == 2
        springTimeFrom_spinner.enabled = a
        springTimeFrom_spinner.value = animationRange.start
        springTimeTo_spinner.enabled = a
        springTimeTo_spinner.value = animationRange.end

    )

    on springTimeFrom_spinner changed val do(
        if springTimeTo_spinner.value <= springTimeFrom_spinner.value then(
            springTimeTo_spinner.value = springTimeFrom_spinner.value+1
        )
--         sm_updateTimeRange()
    )

    on springTimeTo_spinner changed val do(
        if springTimeTo_spinner.value <= springTimeFrom_spinner.value then(
            springTimeFrom_spinner.value = springTimeTo_spinner.value-1
        )
--         sm_updateTimeRange()
    )

    on springColActive_checkbox changed state do(
        springTimeSubFrame_spinner.enabled = springColActive_checkbox.state
        springTimeWipe_checkbox.enabled = springColActive_checkbox.state
        springColFast_checkbox.enabled = springColActive_checkbox.state
        springColFastNum_spinner.enabled = springColActive_checkbox.state
        springTension_spinner.enabled = springColActive_checkbox.state
    )

    on springIsTargetFrame_checkbox changed state do(
        springTargetFrame_spinner.enabled = springIsTargetFrame_checkbox.state
    )

    on springColAddBody_btn pressed  do(
        if $Selection.count == 0 then(createColCapsule undefined)
        else(
            bodyLst = #()
            for obj in $Selection where (ClassOf obj == Biped_Object or ClassOf obj == BoneGeometry) do(
                append bodyLst (createColCapsule obj)
            )
            select bodyLst
        )
    )
    on springColAddPlane_btn pressed  do(
        sm_createColPlane()
    )

    on springColClear_btn pressed  do(
        try(
            delete (getObjWithName colBody_subFix)
        )catch()
        try(
            delete (getObjWithName colPlane_subFix)
        )catch()
    )
    on springBoneInit_btn pressed do(
        sm_initBoneTM $Selection
    )
    on springBoneStraight_btn pressed  do(
        sm_straightBone()
    )
    on springBoneCopy_btn pressed  do(
        sm_copyBoneTM $Selection
    )
    on springBonePaste_btn pressed  do(
        sm_pasteBoneTM $Selection
    )
    on springConnect_btn pressed  do(
        sm_connectBone $Selection
    )
--     on springPick_btn pressed  do(
    
--     )
    on springWind_btn pressed  do(
        sm_createWindCone()
    )
    on springExp_btn pressed  do(
        sm_createExpCtrl()
    )
    on springApply_btn pressed  do(
        springMax_Launcher()
    )
    on springMiscUpdate_btn pressed  do(
        shellLaunch downloadLink ""
    )
    on springMiscBili_btn pressed  do(
        shellLaunch zhTutorialsLink ""
    )
    on springMiscYoutu_btn pressed  do(
        shellLaunch enTutorialsLink ""
    )
    on springMiscLan_btn pressed  do(
        sm_toggleLanUI()
    )
    on springMiscLink_btn pressed  do(
        shellLaunch linkedLink ""
    )
    on springMiscShelf_btn pressed  do(
        actionMan.executeAction 0 "40472"
        print("fileIn (@\""+(getThisScriptFilename())+"\")")
    )
    on spingLanCHN_btn pressed  do(
        sm_toggleLanUI()
        sm_setLan "CHN"
    )
    on spingLanENG_btn pressed  do(
        sm_toggleLanUI()
        sm_setLan "ENG"
    )
)


-- kill exist tool window
try(closeRolloutFloater springMaxWindow)catch()
springMaxWindow = newRolloutFloater (uistrLst[1][2]+" "+(sm_ver as string)+uistrLst[1][3]) floaterWidth floaterHeight 360 160

addRollout sm_Donate springMaxWindow
addRollout sm_SpringMax springMaxWindow
sm_Donate.open = False
sm_SpringMax.open = True



